#include <doctest.h>

#include "WPTextRenderer.hpp"
#include "SystemFontFallback.hpp"

#include <string>
#include <vector>

using namespace wallpaper;

// -------- per-glyph bitmap LRU --------
//
// Layered on top of the existing FT_Face LRU.  Per-(face, pixelSize,
// glyphIdx) cache holds an owned copy of the FT_LOAD_RENDER 8-bit gray
// bitmap + pixel metrics + EffectiveAdvance.  Both MeasureLineWidth and
// the RenderText raster loop go through acquireGlyphLocked, so the second
// occurrence of a glyph in a single line — and every subsequent RenderText
// call with the same text on the same face/size — hits the cache.
//
// Critical correctness invariant: every CachedGlyph holds a raw FT_Face
// pointer.  When the face LRU evicts an entry (or Shutdown / fallback
// purge runs), the glyph cache MUST drop matching entries BEFORE the
// FT_Done_Face call, otherwise the next acquire would hit a freed face
// pointer.  Case 4 below forces that exact sequence.

namespace
{
std::string loadHostFont() {
    const std::string path = wallpaper::ResolveSystemFontFallback("systemfont_sans");
    if (path.empty()) return {};
    return wallpaper::ReadSystemFile(path);
}
} // namespace

TEST_SUITE("WPTextRenderer GlyphCache") {
    TEST_CASE("re-rendering the same text hits the cache on the second pass") {
        // Verifies the load-bearing invariant: a steady-state clock-style
        // wallpaper that ticks the same digits and colon every second
        // doesn't re-run FT_Load_Glyph after the first RenderText call.
        // Hits accumulate from BOTH MeasureLineWidth and the raster loop;
        // per glyph the second call adds two hits (1 measure + 1 raster).
        auto fontData = loadHostFont();
        if (fontData.empty()) {
            MESSAGE("Liberation Sans not present on host; skipping");
            return;
        }
        // Start from a clean global state.  Prior tests leave FT_Face
        // entries in the LRU keyed on raw pointers into their fontData
        // buffers — buffers that go out of scope when the prior test
        // returns.  When a fresh loadHostFont() call reuses one of those
        // freed slabs at the same address and size (malloc free-list
        // coincidence), acquireFaceLocked would hit a STALE face built
        // from the prior caller's (possibly corrupt) bytes, every glyph
        // load would fail, and the cache size + hits would stay at zero.
        // Shutdown drops every cached face, so the new fontData definitely
        // misses and gets a freshly created FT_Face.
        WPTextRenderer::Shutdown();
        WPTextRenderer::Init();
        // First RenderText — all glyphs cold miss.
        (void)WPTextRenderer::RenderText(
            fontData, 16.f, "AB", 64, 32, "center", "center", 0);
        const std::size_t hitsAfterFirst = WPTextRenderer::TEST_getGlyphCacheHits();
        // Second RenderText — both glyphs should now be hot.  Measure +
        // raster each add a hit per glyph → +4 across the line.  Defend
        // the lower bound at 2 (one path could in principle skip the
        // measure if the line is empty; this line isn't, so >= 2 is the
        // load-bearing guarantee that we're not re-loading every glyph).
        (void)WPTextRenderer::RenderText(
            fontData, 16.f, "AB", 64, 32, "center", "center", 0);
        const std::size_t hitsAfterSecond = WPTextRenderer::TEST_getGlyphCacheHits();
        CHECK(hitsAfterSecond - hitsAfterFirst >= 2);
        WPTextRenderer::Shutdown();
    }

    TEST_CASE("repeated glyph within a single line hits after the first occurrence") {
        // A string of 5 identical 'A' glyphs runs through the measure
        // path twice (5 measure hits + 5 raster hits in principle, but
        // the first measure of the first glyph is a miss).  After the
        // first miss, 4 measure hits + 5 raster hits = 9 — defend at
        // >= 4 (a conservative lower bound across plausible code paths).
        auto fontData = loadHostFont();
        if (fontData.empty()) {
            MESSAGE("Liberation Sans not present on host; skipping");
            return;
        }
        // Shutdown drops every FT_Face left over from prior tests so the
        // face cache cannot return a stale entry whose pointer-keyed
        // bytes were freed (see the long comment in the "re-rendering"
        // case for the malloc-address-reuse mechanic).
        WPTextRenderer::Shutdown();
        WPTextRenderer::Init();
        (void)WPTextRenderer::RenderText(
            fontData, 16.f, "AAAAA", 64, 32, "center", "center", 0);
        CHECK(WPTextRenderer::TEST_getGlyphCacheHits() >= 4);
        WPTextRenderer::Shutdown();
    }

    TEST_CASE("cap enforced at 256 entries even with very wide unique-glyph strings") {
        // Wide string of distinct Latin codepoints to exceed the cap.
        // Liberation Sans covers most of Latin-1 + Latin Extended; a
        // string of 0x20..0x14F yields >256 distinct glyph indices.
        // We render in one big string and one fat canvas; the assertion
        // is purely on the cache size invariant.
        auto fontData = loadHostFont();
        if (fontData.empty()) {
            MESSAGE("Liberation Sans not present on host; skipping");
            return;
        }
        // Shutdown drops every FT_Face left over from prior tests so the
        // face cache cannot return a stale entry (see the long comment
        // in the "re-rendering" case).
        WPTextRenderer::Shutdown();
        WPTextRenderer::Init();
        // Build a UTF-8 string of ~300 distinct codepoints starting at 'A'.
        // U+0041..U+0170 (Latin-A + Latin Ext-A) — many of which are in a
        // typical font.  Some may collapse to a single FT glyph index in
        // fonts that share variation selectors / ligatures; we just need
        // *more* than 256 distinct indices, and a 300-codepoint string of
        // Latin alphabetic + accented chars overshoots that comfortably.
        std::string s;
        for (uint32_t cp = 0x41; cp < 0x41 + 300; ++cp) {
            if (cp < 0x80) {
                s.push_back(static_cast<char>(cp));
            } else if (cp < 0x800) {
                s.push_back(static_cast<char>(0xC0 | (cp >> 6)));
                s.push_back(static_cast<char>(0x80 | (cp & 0x3F)));
            } else {
                s.push_back(static_cast<char>(0xE0 | (cp >> 12)));
                s.push_back(static_cast<char>(0x80 | ((cp >> 6) & 0x3F)));
                s.push_back(static_cast<char>(0x80 | (cp & 0x3F)));
            }
        }
        // Wide canvas so most glyphs land inside the clip rect; the cache
        // is unaffected by clipping but a hard min on the size is helpful
        // for diagnosing the test.
        (void)WPTextRenderer::RenderText(
            fontData, 16.f, s, 4096, 64, "left", "top", 0);
        CHECK(WPTextRenderer::TEST_getGlyphCacheSize() <= 256);
        WPTextRenderer::Shutdown();
    }

    TEST_CASE("face eviction purges that face's glyph cache entries") {
        // CRITICAL INVARIANT: when the FT_Face LRU evicts a face, every
        // glyph entry keyed on that face MUST be dropped BEFORE
        // FT_Done_Face — otherwise the cache holds a dangling pointer
        // and the next acquireGlyphLocked would hit on the freed face.
        //
        // Test strategy: render with face A (which inserts an entry into
        // the face LRU and seeds the glyph cache).  Force-evict A by
        // rendering with N > kFaceCacheCapacity distinct font buffers.
        // Re-render with face A — but A has been re-created via
        // FT_New_Memory_Face on the second pass, so the NEW FT_Face is a
        // DIFFERENT pointer than the original.  Since the glyph cache was
        // purged on A's eviction, the new pointer's glyphs all cold-miss.
        //
        // The assertion is on the cache state, not on render correctness
        // (which is already covered by the existing FT_Face cache suite).
        auto fontData = loadHostFont();
        if (fontData.empty()) {
            MESSAGE("Liberation Sans not present on host; skipping");
            return;
        }
        // Shutdown drops every FT_Face left over from prior tests so the
        // face cache cannot return a stale entry (see the long comment
        // in the "re-rendering" case).
        WPTextRenderer::Shutdown();
        WPTextRenderer::Init();
        // Each std::string copy has a unique .data() pointer — that's
        // the face-cache key.  The test renders 'X' with each distinct
        // buffer, then re-renders with the FIRST buffer.
        const std::size_t cap = WPTextRenderer::TEST_getFaceCacheCapacity();
        REQUIRE(cap > 0);
        std::vector<std::string> buffers;
        buffers.reserve(cap + 2);
        for (std::size_t i = 0; i < cap + 2; ++i) buffers.push_back(fontData);
        for (std::size_t i = 1; i < buffers.size(); ++i) {
            REQUIRE(buffers[i].data() != buffers[0].data());
        }
        // First render: face A enters the LRU, 'X' enters the glyph cache.
        (void)WPTextRenderer::RenderText(
            buffers[0], 16.f, "X", 32, 32, "center", "center", 0);
        const std::size_t sizeAfterA = WPTextRenderer::TEST_getGlyphCacheSize();
        CHECK(sizeAfterA >= 1);
        // Render with cap+1 more distinct buffers, evicting face A by
        // the time we're done (the face LRU is FIFO at capacity).
        for (std::size_t i = 1; i < buffers.size(); ++i) {
            (void)WPTextRenderer::RenderText(
                buffers[i], 16.f, "X", 32, 32, "center", "center", 0);
        }
        // After eviction the OLD FT_Face for buffers[0] has been freed +
        // its glyph cache entries purged.  Re-rendering buffers[0] now
        // creates a NEW FT_Face (different pointer) and cold-misses on
        // 'X'.  Snapshot the hit counter before and after the re-render
        // — if the dangling-pointer bug existed, the new acquire would
        // see the OLD face's stale cache entry (since the new FT_Face
        // pointer may collide with the freed memory — heap address reuse)
        // and report a hit, plus we'd be reading freed memory under the
        // sanitizer.
        const std::size_t hitsBefore = WPTextRenderer::TEST_getGlyphCacheHits();
        (void)WPTextRenderer::RenderText(
            buffers[0], 16.f, "X", 32, 32, "center", "center", 0);
        const std::size_t hitsAfter = WPTextRenderer::TEST_getGlyphCacheHits();
        // 'X' on the new face is a cold miss for the first acquire, then
        // every subsequent acquire of that same glyph index is a hit.
        // Measure: 1 miss.  Raster: 1 hit (right after the measure miss
        // populated the cache).  We assert exactly that pattern.
        CHECK((hitsAfter - hitsBefore) <= 1);
        WPTextRenderer::Shutdown();
    }

    TEST_CASE("Shutdown clears the glyph cache (no leak across init cycles)") {
        // Shutdown calls purgeFaceCacheLocked which now bulk-purges
        // the glyph cache before FT_Done_Face on every face.  Verify
        // the post-Shutdown state is empty.
        auto fontData = loadHostFont();
        if (fontData.empty()) {
            MESSAGE("Liberation Sans not present on host; skipping");
            return;
        }
        // Shutdown drops every FT_Face left over from prior tests so the
        // face cache cannot return a stale entry (see the long comment
        // in the "re-rendering" case).
        WPTextRenderer::Shutdown();
        WPTextRenderer::Init();
        (void)WPTextRenderer::RenderText(
            fontData, 16.f, "AB", 64, 32, "center", "center", 0);
        CHECK(WPTextRenderer::TEST_getGlyphCacheSize() >= 1);
        WPTextRenderer::Shutdown();
        // After Shutdown the glyph cache is empty.  TEST_clearGlyphCache
        // also resets the hit counter, but we don't re-arm it here —
        // the load-bearing assertion is just "size == 0".
        CHECK(WPTextRenderer::TEST_getGlyphCacheSize() == 0);
        // Re-Init + re-render survives — proves there is no stale state
        // wedged in the cache from the previous lifecycle (this would
        // catch a bug where the LRU's iterator-map wasn't fully cleared).
        WPTextRenderer::Init();
        (void)WPTextRenderer::RenderText(
            fontData, 16.f, "AB", 64, 32, "center", "center", 0);
        CHECK(WPTextRenderer::TEST_getGlyphCacheSize() >= 1);
        WPTextRenderer::Shutdown();
        CHECK(WPTextRenderer::TEST_getGlyphCacheSize() == 0);
    }
}
